//
// Created by Yucheng on 5/23/17.
//

#ifndef DIYLANE_CLIENT_H
#define DIYLANE_CLIENT_H

#include <string>
#include <curl/curl.h>
#include "worker_pool.hpp"
#include "modules/cookies.hpp"

namespace diylane
{
namespace http
{

struct Request
{
    std::string url = "";
    const char *body = NULL;
    size_t body_size = 0;
    //size_t read_size = 0;
    FILE *file = NULL;
    std::string user_agent = "";
    CookieJar cookieJar;

    Request()
    {
    }

    ~Request()
    {
    }
};

struct Response
{
    std::string error;
    char *memory = NULL;
    size_t size = 0;

    Response()
    {
        memory = (char *)malloc(1);
    }

    ~Response()
    {
        if (memory)
        {
            free(memory);
            memory = NULL;
        }
    }
};

struct Client : pool::Worker
{
    CURL *ctx = NULL;

    Client()
    {
        ctx = curl_easy_init();
        curl_easy_setopt(ctx, CURLOPT_TCP_KEEPALIVE, 1L);
        curl_easy_setopt(ctx, CURLOPT_USERAGENT, "Diylane Agent/1.0");
        curl_easy_setopt(ctx, CURLOPT_COOKIEFILE, ""); /* start cookie engine */
    }

    ~Client()
    {
        if (ctx)
        {
            curl_easy_cleanup(ctx);
            ctx = NULL;
        }
    }

    static size_t write_memory_callback(void *contents, size_t size, size_t nmemb, void *userp)
    {
        size_t realsize = size * nmemb;
        Response *resp = (Response *)userp;

        resp->memory = (char *)realloc(resp->memory, resp->size + realsize + 1);
        if (resp->memory == NULL)
        {
            /* out of memory! */
            resp->error += "not enough memory (realloc returned NULL)\r\n";
            return 0;
        }

        memcpy(&(resp->memory[resp->size]), contents, realsize);
        resp->size += realsize;
        resp->memory[resp->size] = 0;

        return realsize;
    }

    static size_t read_file_callback(void *ptr, size_t size, size_t nmemb, void *userp)
    {
        size_t retcode;
        curl_off_t nread;
        Request *req = (Request *)userp;

        /* in real-world cases, this would probably get this data differently
                    as this fread() stuff is exactly what the library already would do
                    by default internally
                    */
        retcode = fread(ptr, size, nmemb, req->file);

        nread = (curl_off_t)retcode;

        fprintf(stderr, "*** We read %" CURL_FORMAT_CURL_OFF_T
                        " bytes from file\n",
                nread);
        return retcode;
    }

    bool doRequest(Request *req, Response &resp)
    {
        CURLcode res;
        struct curl_slist *cookies = NULL;
        struct curl_slist *nc = NULL;

        // Reset
        curl_easy_setopt(ctx, CURLOPT_POST, 0L);
        curl_easy_setopt(ctx, CURLOPT_UPLOAD, 0L);
        curl_easy_setopt(ctx, CURLOPT_PUT, 0L);
        curl_easy_setopt(ctx, CURLOPT_READDATA, NULL);

        // User agent
        if (req->user_agent.length())
        {
            curl_easy_setopt(ctx, CURLOPT_USERAGENT, req->user_agent.c_str());
        }

        // Common
        curl_easy_setopt(ctx, CURLOPT_URL, req->url.c_str());
        curl_easy_setopt(ctx, CURLOPT_WRITEFUNCTION, write_memory_callback);
        curl_easy_setopt(ctx, CURLOPT_WRITEDATA, &resp);

        // upload
        if (req->file)
        {
            curl_easy_setopt(ctx, CURLOPT_READFUNCTION, read_file_callback);
            curl_easy_setopt(ctx, CURLOPT_UPLOAD, 1L);
            curl_easy_setopt(ctx, CURLOPT_PUT, 1L);
            curl_easy_setopt(ctx, CURLOPT_READDATA, req);
        }

        // post
        if (req->body)
        {
            curl_easy_setopt(ctx, CURLOPT_POST, 1L);
            curl_easy_setopt(ctx, CURLOPT_POSTFIELDS, req->body);
            curl_easy_setopt(ctx, CURLOPT_POSTFIELDSIZE, req->body_size);
        }

        // cookies
        if (req->cookieJar.cookies.size())
        {
            curl_easy_setopt(ctx, CURLOPT_COOKIELIST, "ALL");
            std::string cookieString = req->cookieJar.cookieString();
            curl_easy_setopt(ctx, CURLOPT_COOKIE, cookieString.c_str());
        }

        res = curl_easy_perform(ctx);
        if (res != CURLE_OK)
        {
            resp.error += "curl_easy_perform() failed: ";
            resp.error += curl_easy_strerror(res);
            resp.error += "\n";
            return false;
        }

        res = curl_easy_getinfo(ctx, CURLINFO_COOKIELIST, &cookies);
        if (res != CURLE_OK)
        {
            resp.error += "Curl curl_easy_getinfo failed: ";
            resp.error += curl_easy_strerror(res);
            resp.error += "\n";
            return false;
        }
        nc = cookies;
        while (nc)
        {
            int i = 0;
            char *name = NULL;
            char *value = NULL;
            for (char *p = nc->data; *p != 0; p++)
            {
                if (*p == '\t')
                {
                    *p = 0;
                    i++;
                    if (i == 5)
                    {
                        name = p + 1;
                    }
                    if (i == 6)
                    {
                        value = p + 1;
                    }
                }
            }
            req->cookieJar.set(new Cookie(name, value));
            nc = nc->next;
        }
        curl_slist_free_all(cookies);

        return true;
    }
};

namespace client
{

struct Pool : pool::WorkerPool
{

    Pool()
    {
        name = "http-client";
    }

    ~Pool()
    {
    }

    // singleton helper
    static Pool &singleton(void *uniquePtr = NULL)
    {
        static Pool *singletonPointer = NULL;
        if (uniquePtr)
            singletonPointer = (Pool *)uniquePtr;
        return *singletonPointer;
    }

    /// implementation of createOne_()
    ///
    /// @param error 用来接收错误信息
    /// @return <Client*> 返回工作对象指针
    Client *createOne_(std::string &error)
    {
        return new Client();
    }

    /// acquire a worker.
    ///
    /// the returned worker must be released after use
    /// @param error 用来接收错误信息
    /// @return <Client*> 返回工作对象指针
    Client *acquire(std::string &error)
    {
        return (Client *)acquireWorker_(error);
    }
};

struct AcquiredWorker : pool::AcquiredWorker
{
    AcquiredWorker(Pool *_p, std::string &error)
    {
        pool = _p;
        w = _p->acquire(error);
    }
    ~AcquiredWorker() {}
    Client *unwrap()
    {
        return (Client *)w;
    }
};
}
}
}

#endif //DIYLANE_CLIENT_H
